<?php
// This file is part of Meta AGM grading method - https://code.google.com/p/oasychev-moodle-plugins
//
// Meta AGM grading method is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Meta AGM is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Meta AGM.  If not, see <http://www.gnu.org/licenses/>.

defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot.'/grade/grading/form/lib.php');

/**
 * This controller encapsulates the Meta AGM logic.
 *
 * @package    gradingform_metaagm
 * @author     Nikita Kalinin <nixorv@gmail.com>
 * @copyright  2014 Oleg Sychev (Volgograd State Technical University) <oasychev@gmail.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class gradingform_metaagm_controller extends gradingform_controller {
    /**
     * Returns the HTML code displaying the preview of the grading form.
     *
     * @param moodle_page $page the target page.
     * @return string.
     */
    public function render_preview(moodle_page $page) {
        $mcs = $this->get_metacriteria();
        $output = $page->get_renderer('gradingform_metaagm');
        return $output->gradingform_preview($mcs);
    }
    
    /**
     * Deletes the Meta AGM definition and all metacriterion definitions.
     */
    function delete_plugin_definition() {
        global $DB;

        // We don't need to delete fake areas and definitions.
        // The delete_all_for_context() function will do it for us.

        // Delete Meta instances.
        $instances = array_keys(
            $DB->get_records('grading_instances', array('definitionid' => $this->definition->id), '', 'id')
        );
        $DB->delete_records_list('grading_instances', 'id', $instances);

        $DB->delete_records('gradingform_metaagm_areas', array('areaid' => $this->get_areaid()));
    }

    public function delete_definition() {
        return parent::delete_definition(); // TODO: Переместить все подзоны в контекст 1 и удалять их вручную.
    }
    
    function update_definition(stdClass $definition, $usermodified = null) {
        /* 
           RU: Meta AGM готов к использованию только когда есть хотя бы один определенный метакритерий (AGM).
               Статус готовности формы Meta AGM устанавливается только в том случае, если существует минимум один
               определенный метакритерий. В противном случае необходимо задать "черновой" статус, иначе преподаватель
               не сможет оценивать представления студентов, поскольку форма оценивания будет пуста.
           
           EN: The Meta AGM is ready to use only when there is one defined metacriterion (AGM) at least.
               The Meta AGM form readiness status should be set only in case of existance of at least one
               defined metacriterion. Otherwise the Meta AGM status shoud be set to 'draft', otherwise the teacher
               will not be able to grade students submissions because of the Meta AGM form emptiness.
        */
        
        // So we need to count the AGM's with DEFINITION_STATUS_READY.
        $mcs = $this->get_metacriteria();
        $readynumber = 0;
        foreach ($mcs as $mc) {
            $mcman   = get_grading_manager($mc->subareaid);
            $mccntrl = $mcman->get_active_controller();
            if ($mccntrl && $mccntrl->is_form_available()) {
                $readynumber++;
            }
        }
        
        if ($readynumber > 0) {
            $definition->status = self::DEFINITION_STATUS_READY;
        } else {
            $definition->status = self::DEFINITION_STATUS_DRAFT;
        }
        parent::update_definition($definition);
    }
    
    function check_update_metaagm() {
        if ($this->definition === false) {
            parent::update_definition(new stdClass());
            parent::load_definition();
        }
        $this->update_definition($this->definition);
    }
    
    /**
     * Creates a new fake gradeable area the metacriterion's grading method will be defined in.
     * @param string $method The frankenstyle name of the grading method (AGM);
     * @return int Fake area id.
     */
    private function add_fake_area($method) {
        return self::add_fake_area_for(
            $this->get_context()->id,
            $this->get_component(),
            $this->get_area(),
            $method
        );
    }

    /**
     * Creates a new fake gradeable area the metacriterion's grading method will be defined in.
     * @param int $contextid
     * @param string $component
     * @param string $area
     * @param string $method The frankenstyle name of the grading method (AGM);
     * @return int Fake area id.
     */
    private static function add_fake_area_for($contextid, $component, $area, $method) {
        global $DB;

        $fakearea = new stdClass();
        $fakearea->contextid    = $contextid;
        $fakearea->component    = $component;
        $fakearea->areaname     = $area . '_' . sha1(rand().uniqid($area, true));
        $fakearea->activemethod = $method;

        return $DB->insert_record('grading_areas', $fakearea);
    }
    
    /**
     * Deletes the fake area by id;
     * @param int $areaid
     */
    private function delete_fake_area($areaid) {
        global $DB;
    
        $DB->delete_records('grading_areas', array('id' => $areaid));
    }
    
    /**
     * Updates the fake area.
     * @param stdClass $area
     */
    private function update_fake_area($area) {
        global $DB;
        
        return $DB->update_record('grading_areas', $area);
    }
    
    /**
     * Creates new metacriterion for this Meta AGM.
     * @param string $name   Human-readable name.
     * @param string $method Grading method (AGM).
     * @param float  $weight Grade weight for metacriterion.
     * @return int Metacriterion id.
     */
    public function add_metacriterion($name, $method, $weight = 1.0) {
        global $DB;
        
        // Create fake area for this metacriterion.
        $fakeareaid = $this->add_fake_area($method);
        
        // Create metacriterion record.
        $mc = new stdClass();
        $mc->areaid    = $this->get_areaid();
        $mc->subareaid = $fakeareaid;
        $mc->weight    = $weight;
        $mc->name      = $name;
        
        $ins = $DB->insert_record('gradingform_metaagm_areas', $mc);
        
        $this->check_update_metaagm();
        
        return $ins;
    }
    
    /**
     * Returns metacriterions list matching the given $sqlcondition.
     * @param string $sqlcondition WHERE clause.
     * @return array
     */
    private function get_metacriterions_cond($sqlcondition) {
        global $DB;
        
        return $DB->get_records_sql('SELECT {gradingform_metaagm_areas}.*,
                                            {grading_areas}.contextid,
                                            {grading_areas}.component,
                                            {grading_areas}.areaname,
                                            {grading_areas}.activemethod
                                     FROM {gradingform_metaagm_areas}
                                     INNER JOIN {grading_areas}
                                     ON {gradingform_metaagm_areas}.subareaid = {grading_areas}.id
                                     WHERE ' . $sqlcondition);
    }
    
    /**
     * Returns the metacriterions list of current Meta AGM.
     * @return array Metacriterions list.
     */
    public function get_metacriteria() {
        return $this->get_metacriterions_cond('areaid = \'' . $this->get_areaid() . '\'');
    }
    
    /**
     * Returns the metacriterion by id.
     * @param int $id
     * @return stdClass
     */
    public function get_metacriterion($id) {
        $mcs = $this->get_metacriterions_cond('{gradingform_metaagm_areas}.id = \'' . $id . '\'');
        return empty($mcs) ? false : $mcs[$id];
    }
    
    /**
     * Deletes the metacriterion and all it's grading forms.
     * @param stdClass $mc
     */
    public function delete_metacriterion(stdClass $mc) {
        global $DB;
        
        // Remove all defined forms for this metacriterion.
        $mcman = get_grading_manager($mc->subareaid);
        $methods = array_keys($mcman->get_available_methods(false));
        foreach ($methods as $method) {
            $mccntrl = $mcman->get_controller($method);
            $mccntrl->delete_definition();
        }
        
        // Remove metacriterion.
        $DB->delete_records('gradingform_metaagm_areas', array('id' => $mc->id));
        
        // Remove fake area.
        $this->delete_fake_area($mc->subareaid);
        
        $this->check_update_metaagm();
    }

    /**
     * Updates metacriterion settings.
     * @param stdClass $mc
     * @return boolean
     */
    public function update_metacriterion(stdClass $mc) {
        global $DB;
        
        $fakearea = new stdClass();
        $fakearea->id           = $mc->subareaid;
        $fakearea->activemethod = $mc->activemethod;
        
        $upd_mc = $DB->update_record('gradingform_metaagm_areas', $mc);
        $upd_fa = $this->update_fake_area($fakearea);
        
        $this->check_update_metaagm();
        
        return $upd_fa && $upd_mc;
    }
    
    /**
     * If instanceid is specified and grading instance exists and it is created by this rater for
     * this item, this instance is returned.
     * If there exists a draft for this raterid+itemid, take this draft (this is the change from parent)
     * Otherwise new instance is created for the specified rater and itemid
     *
     * @param int $instanceid
     * @param int $raterid
     * @param int $itemid
     * @return gradingform_instance
     */
    public function get_or_create_instance($instanceid, $raterid, $itemid) {
        global $DB;
        if ($instanceid &&
                $instance = $DB->get_record('grading_instances', array('id'  => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) {
            return $this->get_instance($instance);
        }
        if ($itemid && $raterid) {
            $params = array('definitionid' => $this->definition->id, 'raterid' => $raterid, 'itemid' => $itemid);
            if ($rs = $DB->get_records('grading_instances', $params, 'timemodified DESC', '*', 0, 1)) {
                $record = reset($rs);
                $currentinstance = $this->get_current_instance($raterid, $itemid);
                if ($record->status == gradingform_metaagm_instance::INSTANCE_STATUS_INCOMPLETE &&
                        (!$currentinstance || $record->timemodified > $currentinstance->get_data('timemodified'))) {
                    $record->isrestored = true;
                    return $this->get_instance($record);
                }
            }
        }
        return $this->create_instance($raterid, $itemid);
    }

    /**
     * Returns html code to be included in student's feedback.
     *
     * @param moodle_page $page
     * @param int $itemid
     * @param array $gradinginfo result of function grade_get_grades
     * @param string $defaultcontent default string to be returned if no active grading is found
     * @param bool $cangrade whether current user has capability to grade in this context
     * @return string
     */
    public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) {
        $html = '';
        $metacriteria = $this->get_metacriteria();
        foreach ($metacriteria as $mc) {
            $mcmanager = get_grading_manager($mc->subareaid);
            $mccontroller = $mcmanager->get_active_controller();
            if ($mccontroller && $mccontroller->is_form_available()) {
                $html .= html_writer::tag('h4', $mc->name);
                $html .= $mccontroller->render_grade($page, $itemid, $gradinginfo, null, $cangrade);
            }
        }
        $html .= '<br>' . $defaultcontent;
        return $html;
    }

    /**
     * @inheritdoc
     */
    public function get_definition_copy(gradingform_controller $target) {
        global $DB;

        $copy = parent::get_definition_copy($target);

        $metacriteria = $this->get_metacriteria();
        foreach ($metacriteria as $mc) {
            $mcmanager = get_grading_manager($mc->subareaid);
            $mccontroller = $mcmanager->get_active_controller();
            $mcmethod = $mcmanager->get_active_method();

            // Are we copying to shared area or cm?
            if ($target->get_component() != 'core_grading') {
                // $target is a cm, create fake area for it's context.
                $newareaid = self::add_fake_area_for(
                    $target->get_context()->id,
                    $target->get_component(),
                    $target->get_area(),
                    $mcmethod
                );

                if ($mccontroller && $mccontroller->is_form_defined()) {
                    $mcdefinition = $mccontroller->get_definition();
                    $targetarea = get_grading_manager($newareaid);
                    $targetcontroller = $targetarea->get_controller($mcmethod);
                    $targetcontroller->update_definition($mccontroller->get_definition_copy($targetcontroller));
                    $DB->set_field('grading_definitions', 'timecopied', time(), array('id' => $mcdefinition->id));
                }
            } else {
                if ($mccontroller->is_form_defined()) {
                    $mcdefinition = $mccontroller->get_definition();
                    // Grading form may be shared already. It's should not be shared twice.
                    $shareddefinition = $DB->get_record_sql("SELECT * FROM {grading_definitions} defs
                            INNER JOIN {grading_areas} areas ON
                                defs.areaid = areas.id
                            WHERE
                                defs.copiedfromid = ? AND
                                areas.component = 'core_grading'", array($mcdefinition->id));
                    if ($shareddefinition != null) {
                        $newareaid = $shareddefinition->areaid;
                    } else {
                        $newareaid = $mcmanager->create_shared_area($mcmethod);
                        $targetarea = get_grading_manager($newareaid);
                        $targetcontroller = $targetarea->get_controller($mcmethod);
                        $targetcontroller->update_definition($mccontroller->get_definition_copy($targetcontroller));
                        $DB->set_field('grading_definitions', 'timecopied', time(), array('id' => $mcdefinition->id));
                    }
                } else {
                    $newareaid = $mcmanager->create_shared_area($mcmethod);
                }
            }

            $newmc = new stdClass();
            $newmc->areaid = $target->get_areaid();
            $newmc->subareaid = $newareaid;
            $newmc->weight = $mc->weight;
            $newmc->name = $mc->name;
            $DB->insert_record('gradingform_metaagm_areas', $newmc);
        }

        return $copy;
    }


}


/**
 * Class to manage AGM instances.
 *
 * @package    gradingform_metaagm
 * @author     Nikita Kalinin <nixorv@gmail.com>
 * @copyright  2014 Oleg Sychev (Volgograd State Technical University) <oasychev@gmail.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class gradingform_metaagm_instance extends gradingform_instance {

    /**
     * Calculates the grade of whole form.
     * @param array $mcs Metacriterions.
     * @param array $grades Grades from each metacriterion.
     * @return float|int the valid grade from $this->get_controller()->get_grade_range().
     */
    private function calculate_grade(array $mcs, array $grades) {
        $graderange = array_keys($this->get_controller()->get_grade_range());
        if (empty($graderange)) {
            return -1;
        }

        $grade = 0;
        $weightsum = 0;
        foreach ($mcs as $mc) {
            $mcgrade = each($grades);
            $grade += $mcgrade['value'] * $mc->weight;
            $weightsum += $mc->weight;
        }
        
        return $grade / $weightsum;
    }
    
    /**
     * Calculates the grade to be pushed to the gradebook.
     *
     * @return float|int the valid grade from $this->get_controller()->get_grade_range().
     */
    public function get_grade() {
        $controller = $this->get_controller();
        $mcs = $controller->get_metacriteria();
        $grades = array();
        foreach ($mcs as $mc) {
            $mcman   = get_grading_manager($mc->subareaid);
            $mccntrl = $mcman->get_active_controller();
            $mccntrl->set_grade_range($controller->get_grade_range(), $controller->get_allow_grade_decimals());
            $mcinst = $mccntrl->get_or_create_instance(0, $USER->id, $this->get_data('itemid'));
            $grades[] = $mcinst->get_grade();
        }
        return $this->calculate_grade($mcs, $grades);
    }
    
    /**
     * Returns html for form element of type 'grading'.
     *
     * @param moodle_page $page
     * @param MoodleQuickForm_grading $gradingformelement
     * @return string
     */
    public function render_grading_element($page, $gradingformelement) {
        $controller = $this->get_controller();
        $output = $page->get_renderer('gradingform_metaagm');
        $mcs = $controller->get_metacriteria();
        $html = $output->gradingform($mcs, $this, $gradingformelement);
        return $html;
    }
    
    /**
     * Called when teacher submits the grading form:
     * updates the instance in DB, marks it as ACTIVE and returns the grade to be pushed to the gradebook.
     * $itemid must be specified here (it was not required when the instance was
     * created, because it might not existed in draft).
     *
     * @param array $elementvalue
     * @param int $itemid
     * @return int The grade on 0-100 scale.
     */
    public function submit_and_get_grade($elementvalue, $itemid) {
        global $USER;
        
        $controller = $this->get_controller();
        $metacriteria = $controller->get_metacriteria();

        // Determine what metacriteria were used during grading.
        $gradeablemetacriteria = array();
        $mccontrollers = array();
        foreach ($metacriteria as $mc) {
            $mcman = get_grading_manager($mc->subareaid);
            $mccontroller = $mcman->get_active_controller();
            if ($mccontroller && $mccontroller->is_form_available()) {
                $gradeablemetacriteria[] = $mc;
                $mccontrollers[] = $mccontroller;
                $mccontroller->set_grade_range($controller->get_grade_range(), $controller->get_allow_grade_decimals());
            }
        }

        // Get submitted grades.
        $grades = array();
        $mcinstances = array();
        $gradingvalues = array();
        reset($mccontrollers);
        foreach ($gradeablemetacriteria as $mc) {
            $mccontroller = each($mccontrollers)['value'];
            $mcinstances[] = $mccontroller->get_or_create_instance(0, $USER->id, $itemid);
            // REALLY BAD. But there are array of arrays of arrays of arrays! How to handle them correctly?
            $gradingvalues['_metaagm_advancedgrading' . $mc->id] = $_POST['_metaagm_advancedgrading' . $mc->id];
        }

        // Save grades.
        reset($mcinstances);
        foreach ($gradeablemetacriteria as $mc) {
            $mcinst = each($mcinstances)['value'];
            $elementname = '_metaagm_advancedgrading' . $mc->id;
            $grades[] = $mcinst->submit_and_get_grade($gradingvalues[$elementname], $itemid);
        }

        $elementvalue['itemid'] = $itemid;
        $this->update($elementvalue);
        $this->make_active();

        return $this->calculate_grade($gradeablemetacriteria, $grades);
    }
}
